/* 静态资源-CSS 检测策略 */
const css = require('css');
const CONSTS = require('@lib/consts/index');
const Report = require('@lib/utils/report');
const Fs = require('@lib/utils/fs');
const Css = require('@lib/utils/css');
const {
  isValuableArray,
  mergeArrayByField
} = require('@lib/utils/tools');

// 需要写入报告文件的数据
const outputList = [];

class SrCss {
  /**
   * 执行策略的函数
   * @param {*} cssList css文件列表
   * @param {*} extra 额外参数
   * @param {*} cb 回调函数
   */
  static async execute(cssList = [], extra, cb) {
    if (!isValuableArray(cssList)) {
      cb && cb();
      return;
    }
    // 1.执行策略1
    cssList.forEach(item => {
      const { filePath } = item;
      const fileContent = Fs.readFileSync(filePath);
      SrCss.detectImportRule(item, fileContent, outputList);
    });
    // 2.处理其余策略-不同css文件需要先转化合并成css内容，然后再执行策略
    await SrCss._handleOtherStrategy(cssList, '.css');
    await SrCss._handleOtherStrategy(cssList, '.less');
    await SrCss._handleOtherStrategy(cssList, '.sass', '.scss');
    // 因为.styl文件不能import .stylus文件，所以分开处理
    await SrCss._handleOtherStrategy(cssList, '.styl');
    await SrCss._handleOtherStrategy(cssList, '.stylus');
    // 3.整合outputList相同title项
    const newOutputList = mergeArrayByField(outputList, 'title', 'data');
    // 4.输出数据到报告目录下的临时文件
    if (!newOutputList.length) {
      cb && cb();
      return;
    }
    const { index } = extra;
    Report.outputTempFile(
      newOutputList,
      '静态资源-CSS',
      `${index}sr-css-${CONSTS.REPORT_TEMP_FILENAME}`,
      () => {
        cb && cb();
      }
    );
  }

  /**
   * 其余策略处理，需要将css不同扩展名的文件合并后再执行策略
   * @param {*} totalList 总的css文件
   * @param {*} ext css相关文件扩展名
   */
  static async _handleOtherStrategy(totalList, ...ext) {
    const fileList = totalList.filter(item => ext.includes(item.ext));
    const methodsMap = CONSTS.NONCSS_2_CSS_MAP;
    // 主扩展名
    const mainExt = isValuableArray(ext) ? ext[0] : '';
    if (isValuableArray(fileList) && mainExt) {
      let cssContent;
      switch (mainExt) {
        case '.css':
          // css文件内容直接读取再进行合并
          cssContent = fileList.reduce((preVal, item) => {
            const { filePath } = item;
            return preVal + Fs.readFileSync(filePath);
          }, '');
          break;
        case '.less':
        case '.sass':
        case '.styl':
        case '.stylus':
          cssContent = await Css[methodsMap[mainExt]](fileList);
          break;
        default:
          return;
      }
      cssContent = cssContent.trim();
      // 执行其余策略
      if (cssContent) {
        SrCss.detectImportant(cssContent, ext, outputList);
        // 使用 css.parse() 将其解析为 AST
        const ast = css.parse(cssContent);
        SrCss.detectCssHierarchy(ast, ext, outputList);
        SrCss.detectExpensiveProps(ast, ext, outputList);
      }
    }
  }

  /**
   * 策略1：检测CSS文件是否使用了"@import"规则
   * @import的使用会造成等待所有import的CSS加载完成再进行其他操作，这就会造成其他CSS、js加载的阻塞
   * @param {*} cssItem css文件列表每一项
   * @param {*} fileContent 文件内容
   * @param {*} outputList 报告文件的数据列表
   */
  static detectImportRule(cssItem, fileContent, outputList = []) {
    const importRegex = /@import/i;
    const hasImportRule = importRegex.test(fileContent);
    if (hasImportRule) {
      Report.formatAndpushReportData(
        [{
          fileName: cssItem.fileName,
          filePath: cssItem.filePath
        }],
        '使用@import规则',
        outputList
      );
    }
  }

  /**
   * 策略2：检测是否存在属性值使用了!important
   * @param {*} cssContent css样式内容
   * @param {*} ext css相关文件扩展名，可能存在多个
   * @param {*} outputList 报告文件的数据列表
   */
  static detectImportant(cssContent, ext, outputList = []) {
    const regex = /([\w-]+\s*:\s*[^;]+!important)/g;
    const matches = cssContent.match(regex);
    if (matches && isValuableArray(matches)) {
      Report.formatAndpushReportData(
        [{
          ext: `${ext.join('/')}文件<br>`,
          warnMsg: `【!尽量避免】存在${matches.length}处：<br>${matches.join('<br>')}`
        }],
        '使用!important',
        outputList
      );
    }
  }

  /**
   * 策略3：检测是否存在过深的CSS样式层级，超过阈值则给出告警提示
   * @param {*} ast 样式AST抽象树
   * @param {*} ext css相关文件扩展名，可能存在多个
   * @param {*} outputList 报告文件的数据列表
   */
  static detectCssHierarchy(ast, ext, outputList = []) {
    const maxHierarchyLevel = CONSTS.MAX_CSS_HIERARCHY_LEVEL;
    // 1.遍历 AST 中的规则（rules），获取每个选择器并计算其层级数
    const exceedSelectors = [];
    const selectors = ast.stylesheet.rules.map(rule => rule.selectors).flat() || [];
    selectors.forEach(selector => {
      if (selector) {
        const levels = selector.split(' ').length;
        if (levels > maxHierarchyLevel) {
          exceedSelectors.push(`选择器“${selector}”的层级为${levels}`);
        }
      }
    });
    // 2.存在过深的CSS样式层级的选择器，需要记录下来
    if (isValuableArray(exceedSelectors)) {
      Report.formatAndpushReportData(
        [{
          ext: `${ext.join('/')}文件<br>`,
          warnMsg: `【!尽量避免】存在${exceedSelectors.length}处：<br>${exceedSelectors.join('<br>')}`
        }],
        `存在超过${maxHierarchyLevel}层的CSS样式选择器`,
        outputList
      );
    }
  }

  /**
   * 策略4：检测是否使用昂贵的CSS属性和样式规则
   * @param {*} ast 样式AST抽象树
   * @param {*} ext css相关文件扩展名，可能存在多个
   * @param {*} outputList 报告文件的数据列表
   */
  static detectExpensiveProps(ast, ext, outputList = []) {
    const list = [];
    // 1.遍历css样式节点找出使用昂贵的CSS属性和样式规则的地方
    SrCss._traverseFindExpProps(ast, list);
    // 2.使用了昂贵的CSS属性和样式规则列表有值，需要记录下来
    if (isValuableArray(list)) {
      Report.formatAndpushReportData(
        [{
          ext: `${ext.join('/')}文件<br>`,
          warnMsg: `【!谨慎使用】存在${list.length}处：<br>${list.join('<br>')}`
        }],
        '使用昂贵的CSS属性和样式规则',
        outputList
      );
    }
  }

  /**
   * 遍历css样式节点找出使用昂贵的CSS属性和样式规则的地方
   * @param {*} styleNode 样式节点
   * @param {*} list 收集使用了昂贵的CSS属性和样式规则的列表
   */
  static _traverseFindExpProps(styleNode, list) {
    if (styleNode.type === 'rule') {
      // 1.position: fixed 属性
      if (Css.hasFixedPosition(styleNode)) {
        list.push(`选择器“${styleNode.selectors.join(';')}”使用了position: fixed`);
      }
      // 2.nth-child 伪类
      if (Css.hasNthChild(styleNode)) {
        list.push(`选择器“${styleNode.selectors.join(';')}”使用了nth-child伪类`);
      }
      // 检测属性或者属性值 是否属于昂贵属性
      if (isValuableArray(styleNode.declarations)) {
        styleNode.declarations.forEach((decl) => {
          const prop = decl.property || '';
          const val = decl.value || '';
          // 3.检测属性是否属于昂贵属性
          if (prop) {
            const targetProp = CONSTS.EXPENSIVE_CSS_PROPS_VALUES.find(item => prop.toLowerCase().indexOf(item) > -1);
            if (targetProp) {
              list.push(`选择器“${styleNode.selectors.join(';')}”使用了昂贵的CSS属性:${prop.toLowerCase()}`);
            }
          }
          // 4.检测属性值中是否属于昂贵属性
          if (val) {
            const targetVal = CONSTS.EXPENSIVE_CSS_PROPS_VALUES
              .find(item => {
                // 匹配单词边界
                const regex = new RegExp(`\\b${item}\\b`, 'g');
                return regex.test(val.toLowerCase());
              });
            if (targetVal) {
              list.push(`选择器“${styleNode.selectors.join(';')}”的属性${prop.toLowerCase()}使用了昂贵的CSS属性值: ${val.toLowerCase()}`);
            }
          }
        });
      }
    } else if (styleNode.type === 'keyframes') {
      let hasKeyframesExpProp = false;
      // 5.检测 @keyframes 动画中是否使用了昂贵属性
      for (let i = 0; i < styleNode.keyframes.length; i++) {
        const keyframe = styleNode.keyframes[i];
        for (let j = 0; j < keyframe.declarations.length; j++) {
          const decl = keyframe.declarations[j];
          if (decl.property && CONSTS.EXPENSIVE_CSS_PROPS_VALUES.includes(decl.property.toLowerCase())) {
            list.push(`存在@keyframes ${styleNode.name}使用了昂贵的CSS属性:${decl.property.toLowerCase()}`);
            hasKeyframesExpProp = true;
            break;
          }
        }
        // 找到@keyframes xxx使用了昂贵属性并推入列表后就可以退出循环了
        if (hasKeyframesExpProp) {
          break;
        }
      }
    }
    // 继续遍历css样式节点
    for (const key in styleNode) {
      if (styleNode[key] && typeof styleNode[key] === 'object') {
        SrCss._traverseFindExpProps(styleNode[key], list);
      }
    }
  }
}
module.exports = SrCss;
